﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using Microsoft.SharePoint;
using System.Xml;
using System.Xml.Schema;
using System.IO;
using Plossum.CommandLine;

// The Plossum.CommandLine assembly is used by this application to provide command line processing. This code 
// was downloaded from http://www.codeproject.com/KB/recipes/plossum_commandline.aspx

namespace SharePointGurus.Utility
{

    #region Application parameters class
    //This class defines the parameters that the application uses. The class is passed to the Plossum.Commandline
    //constructor to initialize it. Each parameter is declared as a property get/set or as a public field.

    [CommandLineManager(ApplicationName = "Monthly Folders")]
    class Options
    {
        [CommandLineOption(Description = "Displays this help text")]
        public bool Help = false;

        [CommandLineOption(Description = "The URL of the SharePoint site that contains the document library", MinOccurs = 1)]
        public string Url
        {
            get { return mUrl; }
            set
            {
                if (String.IsNullOrEmpty(value))
                    throw new InvalidOptionValueException("The url must not be empty", false);
                //If a trailing '/' is at the end of the url, then SharePoint does not connect to the correct site collection
                mUrl = value.Trim('/');
            }
        }

        [CommandLineOption(Description = "The name of the document library to create the folders in", MinOccurs = 1)]
        public string Library
        {
            get { return mLibrary; }
            set
            {
                if (String.IsNullOrEmpty(value))
                    throw new InvalidOptionValueException("The document library name must not be empty", false);
                mLibrary = value;
            }
        }

        [CommandLineOption(Description = "The location of the XML file that contains the folder structure", MinOccurs = 1)]
        public string Folders
        {
            get { return mFolders; }
            set
            {
                if (String.IsNullOrEmpty(value))
                    throw new InvalidOptionValueException("The folder path must not be empty", false);
                if (!File.Exists(value))
                    throw new InvalidOptionValueException("The folder XML file is not available at " + value, false);
                mFolders = value;
            }
        }

        private string mLibrary;    // the name of the document library where the folders will be created
        private string mFolders;    // the absolute path to the XML file containing the details of the sub folders to create
        private string mUrl;    // the path to the SharePoint subsite that contains the document library
    }
    #endregion

    class MonthlyFolders
    {        
        /// <summary>
        /// Creates a month-specific folder in a SharePoint document library in the format YYYY-MM. 
        /// It then creates a sub-folder structure based on the contents of an XML file. The XML file can also
        /// specify permissions for the subfolders. 
        /// The application will continue to execute if the folder already exists. 
        /// If the folder already exists with custom permissions, and custom permissions have been defined in the XML file
        /// then the XML file permissions will be applied and the old permissions dropped.
        /// If the code experiences a problem with assigning permissions or creating a sub-folder, it continues on to the next operation.
        /// All output is written to the standard console. This can be redirected to a log file using basic commandline syntax
        /// (e.g. "UpdateFolderPermissions.exe >> Logfile.txt")
        /// </summary>
        /// <param name="args"></param>
        static int Main(string[] args)
        {
            SPDocumentLibrary targetLibrary;

            Options parameters = new Options();
            CommandLineParser parser = new Plossum.CommandLine.CommandLineParser(parameters);
            parser.Parse();
            Console.WriteLine(parser.UsageInfo.GetHeaderAsString(78));

            if (parameters.Help)
            {
                Console.WriteLine(parser.UsageInfo.GetOptionsAsString(78));
                return 0;
            }
            else if (parser.HasErrors)
            {
                Console.WriteLine(parser.UsageInfo.GetErrorsAsString(78));
                return -1;
            }

            SPSite targetSite = null;
            SPWeb targetWeb = null; 

            // Connect to the SharePoint document library
            try
            {
                //Return a reference to the Document Library root
                targetSite = new SPSite(parameters.Url);
                targetWeb = targetSite.OpenWeb();
                targetLibrary = (SPDocumentLibrary)targetWeb.Lists[parameters.Library];
            }
            catch (Exception e)
            {
                outputMessage("Cannot access the SharePoint site called " + parameters.Url + " and/or the document library called " + parameters.Library);
                outputMessage("Ensure that these exist and that the account running this code has read/write permissions.");
                outputMessage("Note that this code must be run on the SharePoint server");
                outputMessage(e.Message);

                targetWeb.Dispose();
                targetSite.Dispose();
                return -1;
            }

            //Calculate the folder name for the current month
            string currentMonthFolderName = DateTime.Now.ToString("yyyy-MM");
            
            Folder currentMonthFolder;  //custom class that represents a sharePoint folder

            //Create the current month folder      
            try
            {
                currentMonthFolder = new Folder(currentMonthFolderName, targetLibrary);
                outputMessage(currentMonthFolder.Path + " now exists.");
            }
            catch (Exception e)
            {
                outputMessage("Cannot create/access the current month folder - " + currentMonthFolderName);
                outputMessage(e.Message);
                targetWeb.Dispose();
                targetSite.Dispose();                
                return -1;
            }

            //Now that the monthly folder has been created, retrieve the details of the sub folders defined in the XML file
            XmlDocument folderDetails = new XmlDocument();

            try
            {
                folderDetails = getFolderDetails(parameters.Folders);
            }
            catch (Exception e)
            {
                outputMessage("Problems reading the Folder Configuration information from " + parameters.Folders);
                outputMessage(e.Message);
                targetWeb.Dispose();
                targetSite.Dispose();
                return -1;
            }
                   
            //Assign permissions to the current month folder, if they have been specified
            assignPermissions(currentMonthFolder, folderDetails.FirstChild);

            //Call the recursive method to process all the folder nodes underneath the root folder
            createSubFolders(folderDetails.FirstChild, currentMonthFolder.Path, targetLibrary);

            //Tidy up
            targetLibrary = null;
            targetWeb.Close();
            targetWeb.Dispose();
            targetSite.Dispose();
            folderDetails = null;

            return 0;
        }

        /// <summary>
        /// Read the details of the sub folder configuration into an XMLDocument object.
        /// Validate the XML document against an XML schema to ensure that it is in the correct format.
        /// The location of the XML Schema is retrieved from the application configuration file
        /// </summary>
        /// <param name="folderConfiguration">The path to the XML file defining the folder structure to create</param>
        /// <returns>A valid xml document that conforms to the custom schema. This represents the folder structure to be created</returns>
        private static XmlDocument getFolderDetails(string folderConfiguration)
        {
            XmlDocument folderDetails;
           
            //Open the XML file with the subfolder details and permissions
            try
            {
                folderDetails = new XmlDocument();

                //Retrieve the custom folder XML Schema from the application settings
                string folderSchemaLocation = ConfigurationManager.AppSettings.Get("FolderSchema");

                //Configure an XmlReaderSettings object to validate the XML file based on the custom schema
                XmlReaderSettings xmlReaderSettings = new XmlReaderSettings();
                xmlReaderSettings.ValidationType = ValidationType.Schema;
                xmlReaderSettings.Schemas.Add(null, folderSchemaLocation);

                xmlReaderSettings.ValidationType = ValidationType.Schema;
                xmlReaderSettings.IgnoreProcessingInstructions = true;

                //Use an XmlReader in combination with the XmlReaderSettings to load and validate the XML document
                xmlReaderSettings.ValidationEventHandler += new ValidationEventHandler(ValidationCallBack);
                XmlReader reader = XmlReader.Create(folderConfiguration, xmlReaderSettings);
                folderDetails.Load(reader);
                reader.Close();
            }
            catch (Exception e)
            {
                throw new Exception("Load Folder Configuration", e);
            }
            
            return folderDetails;
        }

        /// <summary>
        /// This Event Handler is called if an error is encountered while validating the folder xml document against the custom schema.
        /// It throws an exception that will result in the application terminating.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void ValidationCallBack(object sender, ValidationEventArgs e) 
        {
            System.Diagnostics.Trace.WriteLine(e.Message);
            throw new Exception("Folder Security validation failed: " + e.Message);
        }

        /// <summary>
        /// Creates all of the subfolders for a folder and optionally assigns permissions. This is a recursive method
        /// </summary>
        /// <param name="currentFolderDetails">An XmlNode that contains an XML representation of the required folder structure and security</param>
        /// <param name="currentPath">The location that the subfolders will be created under</param>
        /// <param name="targetLibrary">The document library the folders will be created in</param>
        static void createSubFolders(XmlNode currentFolderDetails, string currentPath, SPDocumentLibrary targetLibrary)
        {
            //Retrieve the list of immediate subfolders from the XML node
            XmlNodeList folderList = currentFolderDetails.SelectNodes("folder");

            //Loop through each subfolder to create and assign permissions
            foreach (XmlNode subFolderDetails in folderList)
            {
                string subFolderName = string.Empty;
                Folder subFolder;

                //If there is a problem creating or assigning permissions, skip the item and move onto the next
                try
                {
                    subFolderName = subFolderDetails.Attributes["name"].Value;
                    //Create a subfolder
                    subFolder = new Folder(currentPath + subFolderName, targetLibrary);
                    outputMessage(subFolder.Path + " now exists");
                    assignPermissions(subFolder, subFolderDetails);

                    //Call this method recursively to create all subfolders under the current folder
                    createSubFolders(subFolderDetails, subFolder.Path, targetLibrary);
                }
                catch (Exception e)
                {
                    outputMessage("Problems creating or assigning permissions to the subfolder " + currentPath + subFolderName);
                    outputMessage(e.Message);
                }

                
            }

            folderList = null;
        }

        /// <summary>
        /// Assign permissions to a subfolder. This will discard any custom permissions to an existing subfolder
        /// </summary>
        /// <param name="folder">The folder to assign permissions to</param>
        /// <param name="folderDetails">An xml representation of the permissions to be assigned</param>
        static void assignPermissions(Folder folder, XmlNode folderNode)
        {
            //Retrieve the list of permissions to apply to the folder.
            XmlNodeList securityList = folderNode.SelectNodes("permissions/security");
            
            // If security settings have been specified for this folder, break the permission inheritance 
            if (securityList.Count > 0)
                folder.ResetPermissions();

            //Assign permissions to each of the accounts specified in the XML structure
            foreach (XmlNode securitySetting in securityList)
            {
                string account = securitySetting.Attributes["account"].Value;
                string access = securitySetting.Attributes["access"].Value;
                                
                try
                {
                    folder.GrantPermissions(account, access);                  
                }
                catch (Exception e)
                {
                    outputMessage("Could not assign the account " + account + " the " + access + " permission level on the folder " + folder.Path);
                    outputMessage(e.Message);
                }
            }

            securityList = null;
        }

        /// <summary>
        /// Format the output of the message to the console to include a timestamp
        /// </summary>
        /// <param name="message">The message to write to the console</param>
        static void outputMessage(string message)
        {
            Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd hh:mm ") + message);
        }
    }
}
